#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import os

from common.util.exec_utils import exec_ln_cmd
from db2.backup.util.dpf_backup_util import DpfBackupUtil
from db2.backup.util.ha_utils import is_power_rhel_ha
from db2.comm.const import Db2Const
from db2.comm.db2_cmd import get_lang_value
from db2.comm.db2_exception import ErrCodeException
from db2.comm.error_code import Db2ErrCode
from db2.comm.models.restore_models import LogRestoreParam
from db2.comm.util.comm_decorators import handle_restore_error
from db2.comm.util.common_util import Db2CommonUtil
from common.logger import Logger
from common.const import ParamConstant, CMDResult, CopyDataTypeEnum, RestoreTypeEnum
from common.common import output_execution_result_ex, execute_cmd, check_del_dir, is_clone_file_system
from db2.comm.util.dpf_util import DpfUtil
from db2.comm.util.ha_util import HaUtil
from db2.comm.util.param_util import Db2ParamUtil
from db2.restore.dpf.dpf_db_restore_service import DpfDbRestoreService

LOGGER = Logger().get_logger(filename="db2.log")


class SingleRestoreService:
    def __init__(self, pid, job_id, sub_job_id, param_dict):
        self._pid = pid
        self._job_id = job_id
        self._sub_job_id = sub_job_id
        self._param_dict = param_dict
        self.resource_type = DpfBackupUtil.get_cluster_type(param_dict)

    @staticmethod
    def get_rollforward_cmd(full_bak_end_time, log_tar_dir, restore_param):
        restore_time = Db2CommonUtil.get_roll_status_time(restore_param.user_name, restore_param.db_name)
        if full_bak_end_time \
                and DpfDbRestoreService.parse_time_hold_to_sec(restore_time) <= full_bak_end_time:
            rollforward_db_cmd = f'su - {restore_param.user_name} ' \
                                 f'-c "db2 rollforward database {restore_param.db_name} to end of backup ' \
                                 f'and complete overflow log path \({log_tar_dir}\)"'
        else:
            rollforward_db_cmd = f'su - {restore_param.user_name} ' \
                                 f'-c "db2 rollforward database {restore_param.db_name} to {restore_time} ' \
                                 f'using local time and complete overflow log path \({log_tar_dir}\) ' \
                                 f'noretrieve"'
        return rollforward_db_cmd

    @staticmethod
    def build_restore_dbpath_param(db_install_path):
        return f" to {db_install_path}" if db_install_path else ""

    @staticmethod
    def handle_full_copy_path_list(copy_all, resource_type, use_san_client=False, job_id=""):
        # HA性能适配
        if is_power_rhel_ha(resource_type):
            full_copy_path_list = Db2ParamUtil.get_all_full_copy_path_for_incr_and_diff_restore(
                copy_all, use_san_client=use_san_client, job_id=job_id)
        else:
            full_copy_path_list = [Db2ParamUtil.get_full_copy_path_for_incr_and_diff_restore(copy_all, job_id=job_id)]
        return full_copy_path_list

    @staticmethod
    def check_rollforward_pending_after_exec_restore_db(os_user, db_name):
        # 存在执行restore命令成功，但执行前滚时报”Database "xxx" is not in rollforward pending state on node“的情况
        rollforward_pending_val = Db2CommonUtil.get_rollforward_pending_val_of_db(os_user, db_name)
        if not rollforward_pending_val or rollforward_pending_val.upper() != "DATABASE":
            LOGGER.error(f"The rollforward pending value does not match after executing database restore command, "
                         f"database: {db_name}, rollforward pending: {rollforward_pending_val}.")
            raise Exception("The rollforward pending value does not match after executing database restore command")
        LOGGER.info(f"The rollforward pending value is DATABASE after restoring database.")

    @staticmethod
    @handle_restore_error
    def execute_restore_db(restore_db_cmd, encoding=""):
        """执行恢复数据库命令"""
        LOGGER.info(f"Starting restore database, cmd: {restore_db_cmd}.")
        return_code, out, err = execute_cmd(restore_db_cmd, encoding=encoding)
        LOGGER.info(f"Restore database completed, return_code: {return_code}, out info: {out}, err info: {err}.")
        for line in out.split("\n"):
            if return_code == CMDResult.SUCCESS.value or "Restore is successful" in line or "复原成功" in line:
                LOGGER.info(f"Restore database succeeded.")
                return
        LOGGER.error(f"Execute restore database command failed.")
        raise ErrCodeException(Db2ErrCode.FAILED_EXECUTE_COMMAND, *[restore_db_cmd, out])

    @staticmethod
    def execute_rollforward_db(rollforward_db_cmd, encoding=""):
        """执行前滚数据库命令"""
        LOGGER.info(f"Starting rollforward database, cmd: {rollforward_db_cmd}.")
        return_code, out, err = execute_cmd(rollforward_db_cmd, encoding=encoding)
        LOGGER.info(f"Rollforward database completed, return_code: {return_code}, out info: {out}, err info: {err}.")
        if return_code != CMDResult.SUCCESS.value:
            msg = "Execute rollforward database command failed."
            LOGGER.error(msg)
            raise Exception(msg)
        if "The ROLLFORWARD command completed successfully" not in out and "ROLLFORWARD 命令成功完成" not in out:
            msg = "The rollforward database result information is incorrect."
            LOGGER.error(msg)
            raise Exception(msg)
        LOGGER.info(f"Execute rollforward database command success.")

    @staticmethod
    def create_logtarget_dir(left_path, os_user, encoding=""):
        LOGGER.info(f"Start to create log target path.")
        log_tar_dir = os.path.join(left_path, f"restore_log")
        Db2CommonUtil.check_path_list(log_tar_dir)
        if not os.path.exists(log_tar_dir):
            os.mkdir(log_tar_dir)
            os.chown(log_tar_dir,
                     Db2CommonUtil.get_os_user_id_by_os_user(os_user),
                     Db2CommonUtil.get_group_id_by_os_user(os_user))
        else:
            LOGGER.info(f"Start clean log target path.")
            rm_cmd = f"rm -rf {log_tar_dir}/*"
            ret, out, err = execute_cmd(rm_cmd, encoding=encoding)
            LOGGER.info(f"Clean log target path complete. ret: {ret}, out: {out}, err: {err}")
        LOGGER.info(f"Create log target path success.")
        return log_tar_dir

    def restore_post_handle(self):
        HaUtil.handle_unmount_path_for_san(self._param_dict)
        # 清理软连接
        restore_type = Db2ParamUtil.get_restore_type(self._param_dict)
        if restore_type in (RestoreTypeEnum.INCRE_RESTORE.value, RestoreTypeEnum.DIFF_RESTORE.value):
            copies = Db2ParamUtil.parse_copies(self._param_dict)
            full_copy_path = Db2ParamUtil.get_full_copy_path_for_incr_and_diff_restore(copies, self._job_id)
            Db2CommonUtil.clear_soft_link_of_dir(full_copy_path)

        LOGGER.info(f"Start db2 running restore post.")
        return True

    def report_progress_handle(self, cache_path):
        LOGGER.info(f"Start db2 running report progress.")
        file_path = os.path.realpath(os.path.join(cache_path, self._job_id))
        Db2CommonUtil.check_path_list(file_path)
        result_json = Db2CommonUtil.read_json_from_file(file_path)
        rst_path = os.path.join(ParamConstant.RESULT_PATH, f"result{self._pid}")
        Db2CommonUtil.check_path_list(rst_path)
        output_execution_result_ex(rst_path, result_json)
        LOGGER.info(f"Running db2 report progress complete.")
        return True

    def full_restore(self, cache_path, os_user, copy_db_name, db_name):
        copy_all = Db2ParamUtil.parse_copies(self._param_dict)
        full_copy = Db2ParamUtil.get_copies_by_copy_type(copy_all, CopyDataTypeEnum.FULL_COPY.value)[0]
        encoding = get_lang_value(os_user)
        # HA性能适配
        if is_power_rhel_ha(self.resource_type):
            full_copy_path_list = Db2ParamUtil.get_all_full_copy_path_when_restore(self._param_dict)
            Db2CommonUtil.check_path_list(*full_copy_path_list)
            if is_clone_file_system(self._param_dict):
                for copy_path in full_copy_path_list:
                    Db2CommonUtil.modify_path_permissions(copy_path)
            full_copy_path_list = HaUtil.handle_mount_path_for_san(os_user, self._param_dict, full_copy_path_list)
            full_copy_path = ",".join(full_copy_path_list)
        else:
            full_copy_path = Db2ParamUtil.get_full_copy_path_when_restore(self._param_dict)
            Db2CommonUtil.check_injection(full_copy_path)
            if is_clone_file_system(self._param_dict):
                Db2CommonUtil.modify_path_permissions(full_copy_path)
        log_tar_dir = self.create_logtarget_dir(cache_path, os_user, encoding)
        tmp_copy_bak_time = Db2ParamUtil.get_backup_image_timestamp_of_copy(full_copy)
        Db2CommonUtil.disconnect_db_application(os_user, db_name)
        Db2CommonUtil.deactivate_db_status(os_user, db_name)
        new_log_path = DpfUtil.create_new_log_path_for_restore_db(os_user, db_name, self._param_dict)
        Db2CommonUtil.check_path_list(log_tar_dir)
        Db2CommonUtil.check_os_user_with_ex(os_user)
        Db2CommonUtil.check_injection(db_name, copy_db_name, str(tmp_copy_bak_time))
        if Db2ParamUtil.is_new_location_restore(self._param_dict):
            db_install_path = Db2CommonUtil.get_db_info_dict(os_user).get(db_name, {}).get(
                "localDatabaseDirectory", "")
            db_path_cmd = SingleRestoreService.build_restore_dbpath_param(db_install_path)
            restore_cmd = f'su - {os_user} -c "db2 restore db {copy_db_name} ' \
                          f'from {full_copy_path} ' \
                          f'taken at {tmp_copy_bak_time} ' \
                          f'{db_path_cmd} ' \
                          f'into {db_name} ' \
                          f'logtarget {log_tar_dir} ' \
                          f'newlogpath {new_log_path} without prompting"'
        else:
            restore_cmd = f'su - {os_user} -c "db2 restore db {db_name} ' \
                          f'FROM {full_copy_path} ' \
                          f'taken at {tmp_copy_bak_time} ' \
                          f'logtarget {log_tar_dir} ' \
                          f'newlogpath {new_log_path} without prompting"'
        LOGGER.info(f"Start exec restore data cmd: {restore_cmd}")
        self.execute_restore_db(restore_cmd, encoding)
        roll_cmd = f"su - {os_user} -c 'db2 rollforward database {db_name} to end of backup and complete" \
                   f" overflow log path \({log_tar_dir}\)'"
        self.clear_archive_log_dir(db_name, os_user)
        self.execute_rollforward_db(roll_cmd, encoding)
        check_del_dir(log_tar_dir)
        LOGGER.info(f"Full db2 restore running complete.")

    def diff_restore(self, cache_path, os_user, copy_db_name, db_name):
        """
        差异恢复：将差异副本和增量全部软连接到全量副本目录下。
        """
        Db2CommonUtil.check_os_user_with_ex(os_user)
        encoding = get_lang_value(os_user)
        copy_all = Db2ParamUtil.parse_copies(self._param_dict)
        use_san_client = Db2ParamUtil.use_san_client_for_restore(self._param_dict)
        full_copy_path_list = SingleRestoreService.handle_full_copy_path_list(
            copy_all, self.resource_type, use_san_client=use_san_client, job_id=self._job_id)
        diff_copy_path = Db2ParamUtil.get_diff_copy_path(copy_all, use_san_client=use_san_client, job_id=self._job_id)
        full_copy_path_list = self.build_full_copy_path(cache_path, full_copy_path_list, [diff_copy_path], os_user)
        # 将差异副本文件软连接到全量副本目录
        for tmp_f_n in os.listdir(diff_copy_path):
            tmp_abs_path = os.path.realpath(os.path.join(diff_copy_path, tmp_f_n))
            if os.path.isfile(tmp_abs_path):
                Db2CommonUtil.create_soft_link(tmp_abs_path, os.path.join(full_copy_path_list[0], tmp_f_n))

        full_copy = Db2ParamUtil.get_copies_by_copy_type(copy_all, CopyDataTypeEnum.FULL_COPY.value)[0]
        diff_copy = Db2ParamUtil.get_copies_by_copy_type(copy_all, CopyDataTypeEnum.DIFF_COPY.value)[0]
        Db2CommonUtil.disconnect_db_application(os_user, db_name)
        Db2CommonUtil.deactivate_db_status(os_user, db_name)
        # 手动恢复顺序：差异->全量->差异
        manual_restore_copies = [diff_copy, full_copy, diff_copy]
        # 新位置
        db_path_cmd = ""
        is_new_loc = Db2ParamUtil.is_new_location_restore(self._param_dict)
        if is_new_loc:
            db_install_path = Db2CommonUtil.get_db_info_dict(os_user).get(db_name, {}).get(
                "localDatabaseDirectory", "")
            db_path_cmd = SingleRestoreService.build_restore_dbpath_param(db_install_path)
        new_log_path = DpfUtil.create_new_log_path_for_restore_db(os_user, db_name, self._param_dict)
        log_tar_dir = self.create_logtarget_dir(cache_path, os_user, encoding)
        full_copy_path = ",".join(full_copy_path_list)
        restore_num = len(manual_restore_copies)
        Db2CommonUtil.check_injection(db_name, full_copy_path, new_log_path, db_path_cmd, log_tar_dir, copy_db_name)
        for idx, tmp_restore_copy in enumerate(manual_restore_copies):
            # 只在最后一次restore时提取日志
            log_target_cmd = f"logtarget {log_tar_dir} " if idx == (restore_num - 1) else ""
            tmp_copy_bak_time = Db2ParamUtil.get_backup_image_timestamp_of_copy(tmp_restore_copy)
            Db2CommonUtil.check_injection(str(tmp_copy_bak_time))
            if is_new_loc:
                tmp_restore_db_cmd = f'su - {os_user} -c "db2 restore db {copy_db_name} incremental ' \
                                     f'from {full_copy_path} taken at {tmp_copy_bak_time} {db_path_cmd} ' \
                                     f'into {db_name} {log_target_cmd}newlogpath {new_log_path} without prompting"'
            else:
                tmp_restore_db_cmd = f'su - {os_user} -c "db2 restore db {db_name} incremental ' \
                                     f'from {full_copy_path} taken at {tmp_copy_bak_time} {log_target_cmd}' \
                                     f'newlogpath {new_log_path} without prompting"'
            self.execute_restore_db(tmp_restore_db_cmd, encoding)

        self.check_rollforward_pending_after_exec_restore_db(os_user, db_name)
        self.clear_archive_log_dir(db_name, os_user)

        rollforward_db_cmd = f'su - {os_user} -c "db2 rollforward database {db_name} ' \
                             f'to end of backup and complete overflow log path \({log_tar_dir}\)"'
        self.execute_rollforward_db(rollforward_db_cmd, encoding)
        check_del_dir(log_tar_dir)

    def incr_restore(self, cache_path, os_user, copy_db_name, db_name):
        encoding = get_lang_value(os_user)
        copy_all = Db2ParamUtil.parse_copies(self._param_dict)
        use_san_client = Db2ParamUtil.use_san_client_for_restore(self._param_dict)
        full_copy_path_list = SingleRestoreService.handle_full_copy_path_list(
            copy_all, self.resource_type, use_san_client, job_id=self._job_id)
        Db2CommonUtil.check_injection(*full_copy_path_list)
        incr_copy_paths = Db2ParamUtil.get_incr_copy_paths(copy_all, use_san_client=use_san_client, job_id=self._job_id)
        full_copy_path_list = self.build_full_copy_path(cache_path, full_copy_path_list, incr_copy_paths, os_user)
        Db2CommonUtil.disconnect_db_application(os_user, db_name)
        Db2CommonUtil.deactivate_db_status(os_user, db_name)
        # 将增量副本文件软连接到全量副本目录
        for tmp_incr_copy_path in incr_copy_paths:
            for tmp_f_n in os.listdir(tmp_incr_copy_path):
                tmp_abs_path = os.path.realpath(os.path.join(tmp_incr_copy_path, tmp_f_n))
                if os.path.isfile(tmp_abs_path):
                    Db2CommonUtil.create_soft_link(tmp_abs_path, os.path.join(full_copy_path_list[0], tmp_f_n))
        full_copy = Db2ParamUtil.get_copies_by_copy_type(copy_all, CopyDataTypeEnum.FULL_COPY.value)[0]
        all_incr_copies = Db2ParamUtil.get_copies_by_copy_type(copy_all, CopyDataTypeEnum.INCREMENT_COPY.value)
        # 手动恢复顺序：最后一个增量->全量->依次从第一个增量到最后一个增量
        manual_restore_copies = [all_incr_copies[-1], full_copy] + all_incr_copies
        log_tar_dir = self.create_logtarget_dir(cache_path, os_user, encoding)
        # 新位置
        db_path_cmd = ""
        is_new_loc = Db2ParamUtil.is_new_location_restore(self._param_dict)
        if is_new_loc:
            db_install_path = Db2CommonUtil.get_db_info_dict(os_user).get(db_name, {}).get(
                "localDatabaseDirectory", "")
            db_path_cmd = SingleRestoreService.build_restore_dbpath_param(db_install_path)
        new_log_path = DpfUtil.create_new_log_path_for_restore_db(os_user, db_name, self._param_dict)
        full_copy_path = ",".join(full_copy_path_list)
        restore_num = len(manual_restore_copies)
        Db2CommonUtil.check_os_user_with_ex(os_user)
        Db2CommonUtil.check_injection(db_name, new_log_path)
        for idx, tmp_restore_copy in enumerate(manual_restore_copies):
            # 只在最后一次restore时提取日志
            log_target_cmd = f"logtarget {log_tar_dir} " if idx == (restore_num - 1) else ""
            tmp_copy_bak_time = Db2ParamUtil.get_backup_image_timestamp_of_copy(tmp_restore_copy)
            if is_new_loc:
                tmp_restore_db_cmd = f'su - {os_user} -c "db2 restore db {copy_db_name} incremental ' \
                                     f'from {full_copy_path} taken at {tmp_copy_bak_time} {db_path_cmd} ' \
                                     f'into {db_name} {log_target_cmd}newlogpath {new_log_path} without prompting"'
            else:
                tmp_restore_db_cmd = f'su - {os_user} -c "db2 restore db {db_name} incremental ' \
                                     f'from {full_copy_path} taken at {tmp_copy_bak_time} {log_target_cmd}' \
                                     f'newlogpath {new_log_path} without prompting"'
            self.execute_restore_db(tmp_restore_db_cmd, encoding)
        self.check_rollforward_pending_after_exec_restore_db(os_user, db_name)
        self.clear_archive_log_dir(db_name, os_user)

        rollforward_db_cmd = f'su - {os_user} -c "db2 rollforward database {db_name} ' \
                             f'to end of backup and complete overflow log path \({log_tar_dir}\)"'
        self.execute_rollforward_db(rollforward_db_cmd, encoding)
        check_del_dir(log_tar_dir)

    def build_full_copy_path(self, cache_path, full_copy_path_list, incr_copy_paths, os_user):
        if is_clone_file_system(self._param_dict):
            for tmp_copy_path in full_copy_path_list + incr_copy_paths:
                Db2CommonUtil.modify_path_permissions(tmp_copy_path)
        full_copy_path_list = HaUtil.handle_mount_path_for_san(os_user, self._param_dict, full_copy_path_list)
        if not is_clone_file_system(self._param_dict):
            for db_file in os.listdir(full_copy_path_list[0]):
                # 创建软链接
                exec_ln_cmd(os.path.join(full_copy_path_list[0], db_file), os.path.join(cache_path, db_file))
            full_copy_path_list[0] = cache_path
        return full_copy_path_list

    def clear_archive_log_dir(self, db_name, os_user):
        instance_name = Db2ParamUtil.get_inst_name_of_db_copy(self._param_dict)
        if instance_name:
            try:
                Db2CommonUtil.clear_archive_log_dir(os_user, db_name, instance_name)
            except Exception as ex:
                LOGGER.error(f"Delete archive log fail error:{ex}")
        else:
            LOGGER.error(f"Can not get instance name by param dict")

    def log_restore(self, cache_path, os_user, copy_db_name, db_name):
        encoding = get_lang_value(os_user)
        restore_timestamp = Db2ParamUtil.get_restore_timestamp(self._param_dict)
        LOGGER.info(f"The recovery target timestamp is {restore_timestamp} when executing point-in-time recovery.")
        if not restore_timestamp:
            raise Exception("The recovery target timestamp is empty when executing point-in-time recovery")
        copies = Db2ParamUtil.parse_copies(self._param_dict)
        use_san_client = Db2ParamUtil.use_san_client_for_restore(self._param_dict)
        log_copy_paths = Db2ParamUtil.get_log_copy_paths(copies, use_san_client=use_san_client, job_id=self._job_id)
        if not log_copy_paths:
            LOGGER.error("The list of log copy paths is empty.")
            raise Exception("The list of log copy paths is empty")
        LOGGER.info("Start merging db2 log copies.")
        # 先恢复依赖的全量副本
        full_copy_path = Db2ParamUtil.get_full_copy_path_when_restore(self._param_dict)
        if is_clone_file_system(self._param_dict):
            Db2CommonUtil.modify_path_permissions(full_copy_path)
        full_copy_path = HaUtil.handle_mount_path_for_san(os_user, self._param_dict, [full_copy_path])[0]
        full_copy = Db2ParamUtil.get_copies_by_copy_type(copies, CopyDataTypeEnum.FULL_COPY.value)[0]
        full_bak_time = Db2ParamUtil.handle_backup_image_timestamp_of_copy(full_copy)
        full_bak_end_time = Db2ParamUtil.get_backup_end_time_of_copy(full_copy)
        log_tar_dir = self.create_logtarget_dir(cache_path, os_user, encoding)
        Db2CommonUtil.check_os_user_with_ex(os_user)
        Db2CommonUtil.check_injection(db_name, copy_db_name)
        Db2CommonUtil.disconnect_db_application(os_user, db_name)
        if Db2ParamUtil.is_new_location_restore(self._param_dict):
            db_install_path = Db2CommonUtil.get_db_info_dict(os_user).get(db_name, {}).get("localDatabaseDirectory", "")
            db_path_cmd = SingleRestoreService.build_restore_dbpath_param(db_install_path)
            full_restore_db_cmd = f'su - {os_user} -c "db2 restore db {copy_db_name} ' \
                                  f'from {full_copy_path} ' \
                                  f'taken at {full_bak_time}{db_path_cmd} ' \
                                  f'into {db_name} ' \
                                  f'logtarget {log_tar_dir} ' \
                                  f'newlogpath %s ' \
                                  f'replace existing without prompting"'
        else:
            full_restore_db_cmd = f'su - {os_user} -c "db2 restore db {db_name} ' \
                                  f'from {full_copy_path} ' \
                                  f'taken at {full_bak_time} ' \
                                  f'logtarget {log_tar_dir} ' \
                                  f'newlogpath %s ' \
                                  f'without prompting"'
        restore_param = LogRestoreParam(user_name=os_user, db_name=db_name,
                                        full_restore_db_cmd=full_restore_db_cmd, restore_time=restore_timestamp)
        self.start_exec_rollcmd(restore_param, log_copy_paths, full_bak_end_time,
                                log_tar_dir, cache_path)

    def start_exec_rollcmd(self, restore_param: LogRestoreParam, log_copy_paths, full_bak_end_time,
                           log_tar_dir, cache_path):
        if not log_copy_paths:
            raise Exception("Restore task failed. Log path is null")
        # 将目标数据库的归档目录清空并返回目录
        uid = Db2CommonUtil.get_os_user_id_by_os_user(restore_param.user_name)
        gid = Db2CommonUtil.get_group_id_by_os_user(restore_param.user_name)
        encoding = get_lang_value(restore_param.user_name)
        for tmp_log_path in log_copy_paths:
            chown_cmd = f"chown -R -h {uid}:{gid} {tmp_log_path}"
            _, _, _ = execute_cmd(chown_cmd)
        restore_time = Db2CommonUtil.convert_pitr_timestamp_to_local_time(restore_param.restore_time)
        copies = Db2ParamUtil.parse_copies(self._param_dict)
        if DpfDbRestoreService.is_end_of_log(copies, self._param_dict):
            rollforward_db_cmd = f'su - {restore_param.user_name} ' \
                                 f'-c "db2 rollforward database {restore_param.db_name} to end of logs ' \
                                 f'and complete overflow log path \({log_tar_dir}\) noretrieve"'
        else:
            rollforward_db_cmd = f'su - {restore_param.user_name} ' \
                                 f'-c "db2 rollforward database {restore_param.db_name} to {restore_time} ' \
                                 f'using local time and complete overflow log path \({log_tar_dir}\) noretrieve"'
        # 开始重试恢复：
        complete_flag = False
        retry_nums = 0
        while retry_nums < 2:
            check_del_dir(log_tar_dir)
            log_tar_dir = self.create_logtarget_dir(cache_path, restore_param.user_name, encoding)
            retry_nums += 1
            if retry_nums == 2:
                rollforward_db_cmd = SingleRestoreService.get_rollforward_cmd(full_bak_end_time,
                                                                              log_tar_dir, restore_param)
            LOGGER.info(f"Start try restore. Retry nums: {retry_nums}")
            try:
                new_log_path = DpfUtil.create_new_log_path_for_restore_db(restore_param.user_name,
                                                                          restore_param.db_name, self._param_dict)
                restore_db_cmd = restore_param.full_restore_db_cmd % new_log_path
                self.execute_restore_db(restore_db_cmd, encoding)
            except Exception as e_info:
                LOGGER.error(f"Retry restore failed. retry num: {retry_nums}, error:{e_info}")
                continue
            # 将各日志副本拷贝到全量副本提取日志目录
            for tmp_copy_path in log_copy_paths:
                DpfDbRestoreService.backup_arch_log_to_target_dir(restore_param.user_name, tmp_copy_path, log_tar_dir)
            self.clear_archive_log_dir(restore_param.db_name, restore_param.user_name)
            try:
                self.execute_rollforward_db(rollforward_db_cmd, encoding)
            except Exception as e_info:
                LOGGER.error(f"Retry rollforward failed. retry num: {retry_nums}")
                continue
            complete_flag = True
            break
        if not complete_flag:
            raise Exception("Restore task failed")

    def get_log_copy_min_recover_time(self):
        LOGGER.info(f"Start get log copy minimum recovery time.")
        copies = Db2ParamUtil.parse_copies(self._param_dict)
        log_copy = Db2ParamUtil.get_copies_by_copy_type(copies, CopyDataTypeEnum.LOG_COPY.value)[-1]
        log_bak_time = Db2ParamUtil.handle_backup_image_timestamp_of_copy(log_copy)
        LOGGER.info(f"Get log copy minimum recovery time complete. log time: {log_bak_time}")
        return log_bak_time
